/*
 *  Copyright Shanghai ECLUB Information Technology Co.,Ltd.
 *
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package com.eclub.emotion.rootstone.serializer.io;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.builder.HashCodeBuilder;

import com.eclub.emotion.rootstone.serializer.CommonDataTypeMap;
import com.eclub.emotion.rootstone.serializer.annotation.RsMember;
import com.eclub.emotion.rootstone.serializer.annotation.RsNotMember;
import com.eclub.emotion.rootstone.serializer.annotation.RsSerializable;
import com.eclub.emotion.rootstone.serializer.exception.ForbidSerializeException;

/**
 * 
 * TODO Description
 *
 * @author   jun.yang, Wincor Nixdorf International GmbH
 * @version  $Revision$
 */
class ObjectSerializer implements ISerializerBase {

	@Override
	public void writeObject(Object obj, RsOutput outStream) throws Exception {
		if (obj == null) {
			SerializerFactory.getSerializer(null).writeObject(null, outStream);
			return;
		}
		Class<?> type = obj.getClass();
		TypeInfo typeInfo = getTypeInfo(type);
		outStream.WriteInt32(typeInfo.TypeId);
		if (outStream.WriteRef(obj)) {
			return;
		}
		for (Field f : typeInfo.Fields) {
			Object value = f.get(obj);
			if (value == null) {
				SerializerFactory.getSerializer(null).writeObject(null, outStream);
			} else {
				Class valueType = value.getClass();
				outStream.WriteInt32(CommonDataTypeMap.getTypeId(valueType));
				SerializerFactory.getSerializer(valueType).writeObject(value, outStream);
			}
		}
	}

	@Override
	public Object readObject(RsInput inStream, Class defType) throws Exception {
		int typeId = inStream.ReadInt32();
		if (typeId == 0) {
			return null;
		}
		Class type = CommonDataTypeMap.getClass(typeId);
		if (type == null) {
			throw new ClassNotFoundException("Cannot find class with typeId,target class:" + defType.getName() + ",typeId:" + typeId);
		}
		if (!defType.isAssignableFrom(type) && defType != type) {
			throw new IllegalArgumentException("object must be an instance of " + type.getName());
		}
		byte isRef = (byte) inStream.read();
		int hashcode = inStream.ReadInt32();
		if (isRef > 0) {
			return inStream.GetRef(hashcode);
		}
		TypeInfo typeInfo = getTypeInfo(type);
		Object obj = type.newInstance();
		for (Field f : typeInfo.Fields) {
			int ptypeId = inStream.ReadInt32();
			if (ptypeId == 0) {
				f.set(obj, null);
				continue;
			}
			Class ptype = CommonDataTypeMap.getClass(ptypeId);
			if (ptype == null) {
				throw new ClassNotFoundException("Cannot find class with typId,target class: " + f.getType().getName() + ",typeId:" + ptypeId);
			}
		
			Object value = SerializerFactory.getSerializer(ptype).readObject(inStream, f.getType());
			f.set(obj, value);
		}
		inStream.SetRef(hashcode, obj);
		return obj;
	}

	private static Map<Class<?>, TypeInfo> _typeInfoMap = new HashMap<Class<?>, TypeInfo>();

	private TypeInfo getTypeInfo(Class<?> type) throws ClassNotFoundException, ForbidSerializeException {
		if (_typeInfoMap.containsKey(type)) {
			return _typeInfoMap.get(type);
		}
		RsSerializable cAnn = type.getAnnotation(RsSerializable.class);
		if (cAnn == null) {
			throw new ForbidSerializeException();
		}
		int typeId = CommonDataTypeMap.getTypeId(type);
		TypeInfo typeInfo = new TypeInfo(typeId);
		ArrayList<Field> fields = new ArrayList<Field>();
		Class temType = type;
		while (true) {
			Field[] fs = temType.getDeclaredFields();
			for (Field f : fs) {
				fields.add(f);
			}
			Class superClass = temType.getSuperclass();
			if (superClass == null) {
				break;
			}
			temType = superClass;
		}

		Map<Integer, Field> mapFildes = new HashMap<Integer, Field>();
		List<Integer> indexIds = new ArrayList<Integer>();
		if (cAnn.defaultAll()) {
			for (Field f : fields) {
				RsNotMember ann = f.getAnnotation(RsNotMember.class);
				if (ann != null) {
					continue;
				}
				f.setAccessible(true);
				Integer indexId =HashCodeBuilder.reflectionHashCode(f.getName().toLowerCase());
				mapFildes.put(indexId, f);
				indexIds.add(indexId);
			}
		} else {
			for (Field f : fields) {
				RsMember ann = f.getAnnotation(RsMember.class);
				if (ann == null) {
					continue;
				}
				f.setAccessible(true);
				String name = ann.name();

				if (ann.name() == null || ann.name().length() == 0) {
					name = f.getName();
				}
				Integer indexId = 0;
				if (name.startsWith("#")) {
					indexId = Integer.MAX_VALUE;
				} else {
					indexId = HashCodeBuilder.reflectionHashCode(name.toLowerCase());
				}
				mapFildes.put(indexId, f);
				indexIds.add(indexId);
			}
		}
		int len = indexIds.size();
		for (int i = 0; i < len; i++) {
			for (int j = i + 1; j < len; j++) {
				Integer item = indexIds.get(j);
				if (indexIds.get(i) > item) {
					indexIds.set(j, indexIds.get(i));
					indexIds.set(i, item);
				}
			}
		}
		for (Integer index : indexIds) {
			typeInfo.Fields.add(mapFildes.get(index));
		}
		_typeInfoMap.put(type, typeInfo);
		return typeInfo;
	}
}

class TypeInfo {

	public int TypeId;

	public TypeInfo(int typeId) {
		TypeId = typeId;
	}

	public List<Field> Fields = new ArrayList<Field>();
}
